這篇要來介紹怎麼在實際的環境中使用 Effect ,這次介紹的是在 React 中會怎麼使用,除了簡單的情境我們可以直接用 Efect.runPromise
外,這篇還會介紹到一些其它的場景中要怎麼使用 Effect 寫的程式
你的 React 程式裡可能會有很多的狀態,你也有可能在使用一些全域的狀態管理的套件,所以,在 React 中怎麼呼叫 Effect 呢?就像上面所說的,簡單的情況下我們就用 Effect.runPromise
就行了
import { useQuery } from "@tanstack/react-query";
import { $fetch, type FetchError } from "ofetch";
function fetchPost(id: number) {
return Effect.tryPromise({
try: () => $fetch(`https://jsonplaceholder.typicode.com/posts/${id}`),
catch: (error) => error as FetchError,
});
}
export function MyComponent() {
const { data } = useQuery({
queryKey: ["post", 1],
queryFn: () => Effect.runPromise(fetchPost(1)),
});
return <div>{JSON.stringify(data)}</div>;
}
就像這樣就可以在 React 裡使用了,當你的 Effect 不依賴任何 React 特有的 hooks 或上下文時,這是最直接的方式,接下來我們就來看一些比較複雜的情況
在 React 中我們常會使用全域的狀態管理套件,最有名的像是 Redux ,或是一些其它的解決方案,例如 jotai 或是 zustand ,通常而言這些狀態管理的套件實際上都可以單獨使用與在 React 外部使用,很少有跟 React 完全綁死的,這邊使用 jotai 示範
假設我們有這樣的 store 與 atom
import { atom, createStore } from "jotai";
export const globalDataAtom = atom<number[]>([]);
export const store = createStore();
我們事實上是可以直接在 jotai 外存取 jotai 中的狀態的
// 取得 atom 的內容
store.get(globalDataAtom)
// 設定 atom 的內容
store.set(globalDataAtom, [])
不過通常而言我會偏好包成一個 service 來使用,這樣測試時就可以提供測試用的 store 了
// 提供 jotai 的 store 的 service
export class StoreService extends Effect.Service<StoreService>()("Store", {
accessors: true,
succeed: { store },
}) {}
// 存取 store 裡的資料的 service
export class StoreRepositoryService extends Effect.Service<StoreRepositoryService>()(
"StoreRepository",
{
accessors: true,
effect: Effect.gen(function* () {
const { store } = yield* StoreService;
return {
globalData: Effect.sync(() => store.get(globalDataAtom)),
setGlobalData: (data: number[]) =>
Effect.sync(() => store.set(globalDataAtom, data)),
};
}),
dependencies: [StoreService.Default],
}
) {}
如果今天你要在 Effect 中使用到 React 中的 state ,那我們有個簡單的方法是直接當成參數傳入,或是直接在 Effect 中使用到 React 中的 state ,例如我們提到過,在之前的「15. Effect 實戰分享 3: 資料遷移」中的案例我們使用過 Effect.onExit
來收集成功與失敗的任務數量
const tasks = [
/* 我們的任務,可能有的成功有的失敗 */
];
export function CollectTaskResult() {
const [status, setStatus] = useState({
success: 0,
error: 0,
});
useEffect(() => {
pipe(
tasks,
Effect.allWith({ concurrency: "unbounded", mode: "either" }),
Effect.tap((results) => {
const newStatus = {
success: 0,
error: 0,
};
// 收集結果
for (const result of results) {
if (Either.isRight(result)) {
newStatus.success += 1;
} else {
newStatus.error += 1;
}
}
setStatus(newStatus);
}),
Effect.runPromise
);
}, []);
return <div>{JSON.stringify(status)}</div>;
}
或是我們也可以把 React 中的 state 包成 service ,並在執行時 provide 給 Effect
function Component() {
const [state, setState] = useState();
useEffect(() => {
pipe(
effect,
// 用 service 提供進去
Effect.provide(SomeLayer.Default({ state })),
Effect.runPromise
);
}, []);
return null;
}
既然是前端就需要來看一下打包後的大小的問題, Effect 其實佔了一定的體積,如果我們把上面的範例程式打包一下,然後看一下裡面的各個套件佔的大小的話,你會看到 Effect 大概佔了 42.5kb (gzipped) 跟 react-dom 其實差不多,不過你也可以從這張 treemap 中看到,這邊的 module 的數量比 Effect 真正有的 module 數量要少的很多, Effect 支援 tree shaking 來減少實際包進去的 module 的效果很好
上面這張圖是用 sonda 產生的,這套是我認為非常好用的一個 bundle 大小的分析工具
在前端就常需要做這種決擇,是否要引入一個套件,增加的大小是否會讓網頁的載入速度變的更慢等等,不過我自己的想法是:如果你的流程複雜到需要 Effect 來幫助你管理這些複雜的操作,那你很可能不是在寫那種主要以顯示資料為主的頁面,而是寫一個複雜的應用程式,這種時候,其實你的使用者大概更加在意你的功能的正確性與穩定性,而比較不會在意載入速度,所以視情況大膽的用吧
這邊我們講到了 Effect 在前端使用的情況,下一篇我們要來聊聊在後端可以怎麼應用